﻿using Pdf2epub.ExtractStrategy;
using Pdf2epub.Interfaces;
using Pdf2epub.Model;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Xml;

namespace Pdf2epub.BusinessLogic
{
    internal class HtmlRenderEngine
    {
        private readonly IProjectContext m_ProjectContext;
        private ContentBlock m_LastTextBlock;
        private XmlElement m_LastTextNode;
        private int m_SubChapterCount;

        public HtmlRenderEngine(IProjectContext projectContext)
        {
            m_ProjectContext = projectContext;
            ExportMode = FileExportMode.All;
        }

        public enum FileExportMode
        {
            All,
            SplitAtCapters,
        }

        public FileExportMode ExportMode { get; set; }

        public XmlDocument RenderHtmlFromContentBlocks(ContentBlock[] contentBlocks)
        {
            XmlDocument xmlDoc = new XmlDocument();
            XmlNode htmlNode = CreateHtmlHeader(xmlDoc, null);
            XmlElement bodyElement = xmlDoc.CreateElement("body");
            XmlNode bodyNode = htmlNode.AppendChild(bodyElement);

            RenderContentBlocks(xmlDoc, contentBlocks, bodyNode);

            return xmlDoc;
        }

        public XmlDocument RenderHtmlFromContentCollection(Chapter chapter)
        {
            if (chapter == null)
                return null;

            return RenderHtmlFromContentCollection(new[] { chapter });
        }

        public XmlDocument RenderHtmlFromContentCollection(Chapter[] chapters)
        {
            XmlDocument xmlDoc = new XmlDocument();

            m_SubChapterCount = 0;

            XmlNode htmlNode = CreateHtmlHeader(xmlDoc, chapters);

            XmlElement bodyElement = xmlDoc.CreateElement("body");
            XmlNode bodyNode = htmlNode.AppendChild(bodyElement);

            m_LastTextBlock = null;
            m_LastTextNode = null;

            IEnumerable<ContentBlock> blocksToRender = m_ProjectContext.Content.Where(block => BlockShouldBeRendered(block, chapters)).ToArray();
            RenderContentBlocks(xmlDoc, blocksToRender, bodyNode);

            FillNonPrintableCharactersToReplacementList(blocksToRender);

            return xmlDoc;
        }

        public void TransformToHtml(string targetFolder)
        {
            if (ExportMode == FileExportMode.All)
            {
                CreateStyleSheet(Path.Combine(targetFolder, "styles.css"));

                XmlDocument fullHtml = RenderHtmlFromContentCollection(m_ProjectContext.Content.Chapters);
                string exportPath = Path.Combine(targetFolder, "book.html");
                WriteHtmlToFile(exportPath, fullHtml);
            }
            else
            {
                int chapterNr = 0;

                if (IsCoverImported())
                {
                    RenderCoverHtml(m_ProjectContext, targetFolder, m_ProjectContext.ProjectConfiguration.CoverImagePath);
                }
                else
                {
                    var coverImageBlock = m_ProjectContext.Content.FirstOrDefault(
                        (block =>

                         //block.SourcePage == 1 &&
                         block.ContentType == ContentType.Image &&
                         block.Content.Equals("cover.jpg")));

                    if (coverImageBlock != null)
                    {
                        RenderCoverHtml(targetFolder, coverImageBlock);
                    }
                }

                m_ProjectContext.FeedbackService.SetDetailText("Creating stylesheet");
                CreateStyleSheet(Path.Combine(targetFolder, "styles.css"));

                m_ProjectContext.FeedbackService.SetDetailText("Detecting chapters");

                using (m_ProjectContext.BulkService.Capsule())
                {
                    ChapterDetection chapterDetection = new ChapterDetection(m_ProjectContext);
                    chapterDetection.SetChaptersFromDetectedHeaders();
                }

                foreach (Chapter chapter in m_ProjectContext.Content.Chapters)
                {
                    m_ProjectContext.FeedbackService.SetDetailText(string.Format("Rendering chapter '{0}'", chapter.Headline));

                    chapterNr++;

                    XmlDocument chapterHtml = RenderHtmlFromContentCollection(chapter);
                    if (chapterHtml == null)
                        continue;

                    string chapterFile = Path.Combine(targetFolder, string.Format("Chapter_{0:0000}.html", chapterNr));
                    WriteHtmlToFile(chapterFile, chapterHtml);
                }
            }
        }

        private static void AppendToLastTestNode(ContentBlock contentBlock, XmlText lastTextNode)
        {
            lastTextNode.Value = string.Format("{0} {1}", lastTextNode.Value, contentBlock.Content);
        }

        private static string GetTitle(Chapter[] chapters)
        {
            if (chapters == null)
                return string.Empty;

            if (chapters.Length == 0)
                return string.Empty;

            if (chapters[0] == null)
                return string.Empty;

            if (string.IsNullOrEmpty(chapters[0].Headline))
                return string.Empty;

            return chapters[0].Headline;
        }

        private static void InsertNewTextNode(XmlDocument xmlDoc, ContentBlock contentBlock, XmlElement htmlTag)
        {
            XmlElement spanTag = xmlDoc.CreateElement("span");
            if (!string.IsNullOrEmpty(contentBlock.StyleName))
            {
                spanTag.SetAttribute("class", contentBlock.StyleName.TrimStart('.'));
            }
            htmlTag.AppendChild(spanTag);

            XmlText textNode = xmlDoc.CreateTextNode(contentBlock.Content.Trim('\0'));
            spanTag.AppendChild(textNode);
        }

        private bool BlockShouldBeJoinedWithLastBlock(ContentBlock contentBlock)
        {
            return (contentBlock.JoinWithPreviousBlock && (m_LastTextBlock != null) &&
                    (m_LastTextBlock.HtmlTag == contentBlock.HtmlTag));
        }

        private bool BlockShouldBeRendered(ContentBlock block, IEnumerable<Chapter> chapters)
        {
            return chapters.Any(topChapter =>
                (block.HtmlTag != HtmlReplacementTag.Remove) &&
                (block.Chapter.IsPartOfTopLevelChapter(topChapter)));

            //return chapters.Any(chapter.IsPartOfTopLevelChapter);
        }

        private XmlNode CreateHtmlHeader(XmlDocument xmlDoc, Chapter[] chapters)
        {
            XmlElement htmlElement = xmlDoc.CreateElement("html");
            htmlElement.SetAttribute("xmlns", "http://www.w3.org/1999/xhtml");
            htmlElement.SetAttribute("xml:lang", "en");
            XmlNode htmlNode = xmlDoc.AppendChild(htmlElement);

            XmlElement headerElement = xmlDoc.CreateElement("head");

            XmlElement titleElement = xmlDoc.CreateElement("title");
            XmlText titleText = xmlDoc.CreateTextNode(GetTitle(chapters));
            titleElement.AppendChild(titleText);
            headerElement.AppendChild(titleElement);

            XmlNode headerNode = htmlNode.AppendChild(headerElement);

            XmlElement metaElement = xmlDoc.CreateElement("meta");
            metaElement.SetAttribute("http-equiv", "Content-Type");
            metaElement.SetAttribute("content", "text/html; charset=utf-8");
            headerNode.AppendChild(metaElement);

            XmlElement styleDefinitionElement = RenderStyleDefinitionElement(xmlDoc);
            headerNode.AppendChild(styleDefinitionElement);
            return htmlNode;
        }

        private XmlElement RenderStyleDefinitionElement(XmlDocument xmlDoc)
        {
            XmlElement linkNode = xmlDoc.CreateElement("link");
            linkNode.SetAttribute("rel", "stylesheet");
            linkNode.SetAttribute("type", "text/css");
            linkNode.SetAttribute("href", "styles.css");

            return linkNode;

            //XmlElement styleDefinitionElement = xmlDoc.CreateElement("style");
            //styleDefinitionElement.SetAttribute("type", "text/css");

            //foreach (HtmlReplacement replacement in m_ProjectContext.ProjectConfiguration.HtmlReplacements)
            //{
            //    string styleDefinition = string.Format("{0} {1}\n", replacement.StyleName, replacement.StyleContent);
            //    if ((string.IsNullOrEmpty(styleDefinition.Trim())) || (replacement.Tag == HtmlReplacementTag.Remove))
            //        continue;

            //    XmlText styleTextElement = xmlDoc.CreateTextNode(styleDefinition);
            //    styleDefinitionElement.AppendChild(styleTextElement);
            //}

            //return styleDefinitionElement;
        }

        public void CreateStyleSheet(string filepath)
        {
            using (TextWriter cssWriter = new StreamWriter(filepath))
            {
                foreach (HtmlReplacement replacement in m_ProjectContext.ProjectConfiguration.HtmlReplacements)
                {
                    string styleDefinition = string.Format("{0} {1}\n", replacement.StyleName, replacement.StyleContent);
                    if ((string.IsNullOrEmpty(styleDefinition.Trim())) || (replacement.Tag == HtmlReplacementTag.Remove))
                        continue;

                    cssWriter.WriteLine(styleDefinition);
                }
                cssWriter.Flush();
                cssWriter.Close();
            }
        }

        private void FillNonPrintableCharactersToReplacementList(IEnumerable<ContentBlock> contentBlocksToRender)
        {
            List<char> nonAsciiCharacters = new List<char>();
            foreach (ContentBlock contentBlock in contentBlocksToRender)
            {
                if ((contentBlock.ContentType != ContentType.Image) && (!string.IsNullOrEmpty(contentBlock.Content)) && (contentBlock.Content.Length > 0))
                {
                    nonAsciiCharacters.AddRange(contentBlock.Content.Where(c => c > 0xFF));
                }
            }

            foreach (var character in nonAsciiCharacters.Distinct())
            {
                if (m_ProjectContext.ProjectConfiguration.TextReplacements.Any(rep => rep.OriginalText == character.ToString(CultureInfo.InvariantCulture)))
                    continue;

                TextReplacement textReplacement = new TextReplacement
                {
                    OriginalText = character.ToString(CultureInfo.InvariantCulture),
                    ReplacementText = string.Empty
                };

                m_ProjectContext.ProjectConfiguration.TextReplacements.Add(textReplacement);
            }
        }

        private XmlText FindLastTextNode()
        {
            XmlElement lastSpanNode = m_LastTextNode.ChildNodes.OfType<XmlElement>().LastOrDefault(element => element.Name.Equals("span", StringComparison.InvariantCultureIgnoreCase));
            if (lastSpanNode != null)
            {
                return lastSpanNode.ChildNodes.OfType<XmlText>().Last();
            }
            return null;
        }

        private string GetHtmlTag(HtmlReplacementTag tag)
        {
            if (tag == HtmlReplacementTag.None)
                return HtmlReplacementTag.P.ToString().ToLower(CultureInfo.InvariantCulture);

            return tag.ToString().ToLower(CultureInfo.InvariantCulture);
        }

        private string GetSubChapterId()
        {
            m_SubChapterCount++;
            return string.Format("sub{0}", m_SubChapterCount);

            //return string.Format("{0}", m_SubChapterCount);
        }

        private void InsertNewSpanTextToLastTextNode(XmlDocument xmlDoc, ContentBlock contentBlock)
        {
            XmlElement spanTag = xmlDoc.CreateElement("span");
            spanTag.SetAttribute("class", contentBlock.StyleName.TrimStart('.'));
            m_LastTextNode.AppendChild(spanTag);

            XmlText textNode = xmlDoc.CreateTextNode(contentBlock.Content);
            spanTag.AppendChild(textNode);
        }

        private bool IsCoverImported()
        {
            if (string.IsNullOrEmpty(m_ProjectContext.ProjectConfiguration.CoverImagePath))
                return false;

            string directory = Path.GetDirectoryName(m_ProjectContext.ProjectConfiguration.CoverImagePath);
            return !string.IsNullOrEmpty(directory);
        }

        private bool IsDifferentReplacement(ContentBlock contentBlock)
        {
            if (m_LastTextBlock == null)
                return true;

            return (!string.IsNullOrEmpty(m_LastTextBlock.StyleName)) && (!string.IsNullOrEmpty(contentBlock.StyleName)) &&
                   (!m_LastTextBlock.StyleName.Equals(contentBlock.StyleName));
        }

        private bool IsLineSpreadingHeader(ContentBlock contentBlock)
        {
            if (!contentBlock.HtmlTag.ToString().StartsWith("h", StringComparison.InvariantCultureIgnoreCase))
                return false;

            return ((m_LastTextBlock.ContentType == contentBlock.ContentType) &&
                    (m_LastTextBlock.HtmlTag == contentBlock.HtmlTag));
        }

        private bool IsPageSpreadingParagraph(ContentBlock contentBlock)
        {
            if ((m_LastTextBlock != null) && (m_LastTextNode != null))
            {
                if ((m_LastTextBlock.StyleName.Equals(contentBlock.StyleName)) &&
                    (m_LastTextBlock.SourcePage != contentBlock.SourcePage))
                {
                    if (!string.IsNullOrEmpty(contentBlock.Content))
                    {
                        string firstChar = contentBlock.Content[0].ToString(CultureInfo.InvariantCulture);
                        if (firstChar.ToUpper() != firstChar)
                        {
                            return true;
                        }
                    }
                }
            }
            return false;
        }

        private bool IsSameBaseline(ContentBlock contentBlock)
        {
            if (m_LastTextBlock == null)
                return false;

            return Math.Abs(m_LastTextBlock.LastChunkBaseLine - contentBlock.FirstChunkBaseLine) < (m_ProjectContext.ProjectConfiguration.ParagraphDistance / 2.0f);
        }

        private void ProcessAnchor(XmlDocument xmlDoc, ContentBlock contentBlock, XmlNode headerNode)
        {
            if (HtmlReplacement.AllHeaderTags.Contains(contentBlock.HtmlTag))
            {
                XmlElement anchorElement = xmlDoc.CreateElement("a");
                anchorElement.SetAttribute("id", GetSubChapterId());
                if (headerNode.ChildNodes.Count > 0)
                {
                    headerNode.InsertBefore(anchorElement, headerNode.ChildNodes[0]);
                }
                else
                {
                    headerNode.AppendChild(anchorElement);
                }
            }
        }

        private XmlElement RenderContentBlock(XmlDocument xmlDoc, ContentBlock contentBlock)
        {
            XmlElement htmlTag = null;
            switch (contentBlock.ContentType)
            {
                case ContentType.Text:
                    htmlTag = RenderTextBlock(xmlDoc, contentBlock);
                    break;

                case ContentType.Image:
                    htmlTag = RenderImageBlock(xmlDoc, contentBlock);
                    break;

                case ContentType.LineBreak:
                    htmlTag = RenderLineBreakBlock(xmlDoc);
                    break;

                case ContentType.Table:
                    htmlTag = RenderTable(xmlDoc, contentBlock);
                    break;

                case ContentType.ListBlock:
                    htmlTag = RenderListBlock(xmlDoc, contentBlock);
                    break;
            }
            return htmlTag;
        }

        private void RenderContentBlocks(XmlDocument xmlDoc, IEnumerable<ContentBlock> contentBlocksToRender, XmlNode bodyNode)
        {
            foreach (var contentBlock in contentBlocksToRender)
            {
                XmlElement htmlTag = RenderContentBlock(xmlDoc, contentBlock);
                if (htmlTag != null)
                {
                    ProcessAnchor(xmlDoc, contentBlock, htmlTag);
                    bodyNode.AppendChild(htmlTag);

                    if (contentBlock.DefaultReplacement != null)
                    {
                        bool tagIsLastTextNode = TagIsLastTextNode(htmlTag);

                        XmlElement processedHtmlTag = contentBlock.DefaultReplacement.ExecuteRules(contentBlock, htmlTag) as XmlElement;

                        if (tagIsLastTextNode && (processedHtmlTag != null) && (processedHtmlTag != m_LastTextNode))
                        {
                            m_LastTextNode = processedHtmlTag;
                        }
                    }
                }
            }
        }

        private void RenderCoverHtml(IProjectContext context, string targetFolder, string coverImageFile)
        {
            const string coverFileName = "importedCover.jpg";

            string newCoverPath = Path.Combine(m_ProjectContext.ProjectConfiguration.ImageOutputPath, coverFileName);
            if (File.Exists(newCoverPath))
                File.Delete(newCoverPath);
            File.Copy(coverImageFile, newCoverPath);

            ContentBlock fakeCoverBlock = new ContentBlock(context, ContentType.Image, coverFileName);
            RenderCoverHtml(targetFolder, fakeCoverBlock);
        }

        private void RenderCoverHtml(string targetFolder, ContentBlock coverImageBlock)
        {
            m_ProjectContext.FeedbackService.SetDetailText("Rendering cover");

            XmlDocument coverXmlDoc = new XmlDocument();

            XmlNode htmlNode = CreateHtmlHeader(coverXmlDoc, null);

            SetImageStyleForCover(htmlNode);

            XmlElement bodyElement = coverXmlDoc.CreateElement("body");
            XmlNode bodyNode = htmlNode.AppendChild(bodyElement);

            IEnumerable<ContentBlock> contentBlocksToRender = new[] { coverImageBlock };
            RenderContentBlocks(coverXmlDoc, contentBlocksToRender, bodyNode);

            WriteHtmlToFile(Path.Combine(targetFolder, "acover.xhtml"), coverXmlDoc);
        }

        private XmlElement RenderImageBlock(XmlDocument xmlDoc, ContentBlock contentBlock)
        {
            XmlElement div = xmlDoc.CreateElement("div");

            XmlElement imageTag = xmlDoc.CreateElement("img");
            imageTag.SetAttribute("src", string.Format("{0}/{1}", Controler.ImageOutputSubPath, contentBlock.Content));
            imageTag.SetAttribute("alt", string.Format("image '{0}'", contentBlock.Content));

            div.AppendChild(imageTag);

            return div;
        }

        private XmlElement RenderLineBreakBlock(XmlDocument xmlDoc)
        {
            return xmlDoc.CreateElement("br");
        }

        private XmlElement RenderListBlock(XmlDocument xmlDoc, ContentBlock listBlock)
        {
            if (listBlock.SubBlocks.Count == 0)
                return null;

            XmlElement ulElement = xmlDoc.CreateElement("ul");
            ulElement.SetAttribute("class", listBlock.StyleName.TrimStart('.'));
            foreach (ContentBlock subBlock in listBlock.SubBlocks)
            {
                RenderListElement(xmlDoc, ulElement, subBlock);
            }
            return ulElement;
        }

        private void RenderListElement(XmlDocument xmlDoc, XmlElement ulElement, ContentBlock subBlock)
        {
            XmlElement ilElement = xmlDoc.CreateElement("il");
            ilElement.SetAttribute("class", subBlock.StyleName.TrimStart('.'));
            ilElement.AppendChild(xmlDoc.CreateTextNode(subBlock.Content));
            ulElement.AppendChild(ilElement);
        }

        private XmlElement RenderTable(XmlDocument xmlDoc, ContentBlock contentBlock)
        {
            if (contentBlock.SubBlocks.Count == 0)
                return null;

            XmlElement tableElement = xmlDoc.CreateElement("table");
            foreach (ContentBlock subBlock in contentBlock.SubBlocks)
            {
                switch (subBlock.ContentType)
                {
                    case ContentType.TableCaption:
                        RenderTableCaption(xmlDoc, tableElement, subBlock);
                        break;

                    case ContentType.TableRow:
                        RenderTableRow(xmlDoc, tableElement, subBlock);
                        break;
                }
            }
            return tableElement;
        }

        private void RenderTableCaption(XmlDocument xmlDoc, XmlElement tableElement, ContentBlock captionBlock)
        {
            XmlElement captionElement = xmlDoc.CreateElement("caption");
            captionElement.SetAttribute("class", captionBlock.StyleName.TrimStart('.'));

            XmlText textElement = xmlDoc.CreateTextNode(captionBlock.Content);
            captionElement.AppendChild(textElement);

            tableElement.AppendChild(captionElement);
        }

        private void RendertableCell(XmlDocument xmlDoc, XmlElement rowElement, ContentBlock cellBlock)
        {
            XmlElement cellElement = xmlDoc.CreateElement("td");
            cellElement.SetAttribute("class", cellBlock.StyleName.TrimStart('.'));

            XmlText textElement = xmlDoc.CreateTextNode(cellBlock.Content);
            cellElement.AppendChild(textElement);

            rowElement.AppendChild(cellElement);
        }

        private void RenderTableRow(XmlDocument xmlDoc, XmlElement tableElement, ContentBlock rowBlock)
        {
            XmlElement rowElement = xmlDoc.CreateElement("tr");
            rowElement.SetAttribute("class", rowBlock.StyleName.TrimStart('.'));

            foreach (ContentBlock cellBlock in rowBlock.SubBlocks)
            {
                RendertableCell(xmlDoc, rowElement, cellBlock);
            }

            tableElement.AppendChild(rowElement);
        }

        private XmlElement RenderTextBlock(XmlDocument xmlDoc, ContentBlock contentBlock)
        {
            XmlElement htmlTag = null;
            if (contentBlock.HtmlTag != HtmlReplacementTag.Remove)
            {
                XmlText lastTextNode = null;
                if (m_LastTextNode != null)
                {
                    lastTextNode = FindLastTextNode();
                }

                bool isSameBaseLine = IsSameBaseline(contentBlock);
                bool isDifferentParagraph = IsDifferentReplacement(contentBlock);

                if ((lastTextNode != null) && (ShouldBlockBeAppendedToPreviousOne(contentBlock) || (isSameBaseLine && !isDifferentParagraph)))
                {
                    AppendToLastTestNode(contentBlock, lastTextNode);
                }
                else if ((m_LastTextNode != null) && (lastTextNode != null) && isSameBaseLine)
                {
                    InsertNewSpanTextToLastTextNode(xmlDoc, contentBlock);
                }
                else if ((m_LastTextNode != null) && (m_LastTextNode.Name.Equals("li", StringComparison.InvariantCultureIgnoreCase))
                    && (m_LastTextBlock.FirstChunkBaseLine == contentBlock.FirstChunkBaseLine))
                {
                    var appendTag = xmlDoc.CreateElement(GetHtmlTag(contentBlock.HtmlTag));
                    InsertNewTextNode(xmlDoc, contentBlock, appendTag);
                    m_LastTextNode.AppendChild(appendTag);
                }
                else
                {
                    htmlTag = xmlDoc.CreateElement(GetHtmlTag(contentBlock.HtmlTag));

                    InsertNewTextNode(xmlDoc, contentBlock, htmlTag);

                    m_LastTextNode = htmlTag;
                }
            }

            if (contentBlock.HtmlTag != HtmlReplacementTag.Remove)
            {
                m_LastTextBlock = contentBlock;
            }
            return htmlTag;
        }

        private void SetImageStyleForCover(XmlNode htmlNode)
        {
            XmlNode headNode = htmlNode.ChildNodes.Cast<XmlNode>().FirstOrDefault(node => node.Name.Equals("head", StringComparison.InvariantCultureIgnoreCase));
            if (headNode == null)
                return;

            XmlNode styleNode = headNode.ChildNodes.Cast<XmlNode>().FirstOrDefault(node => node.Name.Equals("style", StringComparison.InvariantCultureIgnoreCase));
            if (styleNode == null)
                return;
            styleNode.InnerText = "img { max-width: 100%; max-height: 100%; }";
        }

        private bool ShouldBlockBeAppendedToPreviousOne(ContentBlock contentBlock)
        {
            if (m_LastTextNode == null)
                return false;

            return (IsPageSpreadingParagraph(contentBlock) ||
                IsLineSpreadingHeader(contentBlock) ||
                BlockShouldBeJoinedWithLastBlock(contentBlock));
        }

        private bool TagIsLastTextNode(XmlElement htmlTag)
        {
            return m_LastTextNode == htmlTag;
        }

        private void WriteHtmlToFile(string exportPath, XmlDocument fullHtml)
        {
            string outDir = Path.GetDirectoryName(exportPath);
            if (!Directory.Exists(outDir))
                Directory.CreateDirectory(outDir);
            fullHtml.Save(exportPath);
        }
    }
}